import type { FormInstance } from 'element-plus'
import type { Ref } from 'vue'
import Schema from 'async-validator'
import type { ValidateFieldsError } from 'async-validator'
import {
  cloneDeep, pick, omit, upperFirst,
} from 'lodash-es'
import { nextTick, reactive } from 'vue'
import type {
  ModelFormat, ModelRules, Model, PageInfo,
} from '@/types'
import { rangeParamsFormat } from '@/utils/ruoyi'

const isPhpService = import.meta.env.VITE_APP_SERVICE_LANG === 'php'

/**
 * 对 reactive 表单对象进行扩展， 包装方法说明请看@/types/table.ts Model
 * @param init
 * @param options
 */
export default function useModel<T extends Record<string, any>>(init: T | (() => T), options: {
  // 通过 getRef 获取实例, 可调用验证和重置方法
  ref?: Ref<FormInstance | undefined> | string
  // 表单验证
  rules?: ModelRules<T>,
  // 对数据进行格式化
  format?: ModelFormat<T> | ((model: T) => ModelFormat<T>)
} = {}) {
  const currentInstance = getCurrentInstance()
  const defaultModel = typeof init === 'function' ? init() : cloneDeep(init)
  const model = cloneDeep(defaultModel) as Model<T>

  Object.defineProperty(model, 'getRef', {
    value() {
      if (typeof options.ref === 'string') {
        return currentInstance?.refs[options.ref] as FormInstance
      }
      return options?.ref?.value
    },
  })

  Object.defineProperty(model, 'getRules', {
    value() {
      return options.rules
    },
  })

  Object.defineProperty(model, 'getFormat', {
    value() {
      return options?.format
    },
  })

  Object.defineProperty(model, 'resetModel', {
    value<K extends T>(value?: Partial<K>, able?: boolean) {
      if (able !== false && options.format) {
        const fmt = typeof options.format === 'function' ? options.format(this) : options.format
        fmt.pageable?.value.reset()
        fmt.sortable?.value.reset()
      }

      Object.entries(init).forEach(([k, v]) => {
        if (value && value[k] !== undefined) {
          this[k] = value[k]
        } else if (options.format) {
          const fmt = typeof options.format === 'function' ? options.format(this) : options.format
          let keys: [string, string] | undefined
          // range 取值还原
          if (fmt.range && value) {
            if (k === fmt.range) {
              keys = ['beginTime', 'endTime']
            } else if (Array.isArray(fmt.range)) {
              const range = fmt.range.find((it) => (Array.isArray(it) ? it[0] : it) === k)
              if (range) {
                if (Array.isArray(range)) {
                  if (range.length === 2) {
                    keys = [`begin${range[1]}`, `end${range[1]}`]
                  } else if (range.length === 3) {
                    keys = [range[1], range[2]]
                  }
                } else if (range === k) {
                  keys = [`begin${upperFirst(k)}`, `end${upperFirst(k)}`]
                }
              }
            }
          }
          const val = value as any
          if (keys && val[keys[0]] !== undefined && val[keys[1]] !== undefined) {
            this[k] = [val[keys[0]], val[keys[1]]]
          } else {
            this[k] = cloneDeep(v)
          }
        } else {
          this[k] = cloneDeep(v)
        }
      })
      if (this.getRef()) {
        nextTick(() => {
          this.getRef().clearValidate()
        }).then()
      }
    },
  })

  Object.defineProperty(model, 'getModel', {
    value<R extends(T & Record<string, any>)>(format: ModelFormat<T> = {}): R {
      const res = Object.fromEntries(
        Object.keys(defaultModel).map((key) => {
          return [key, cloneDeep(this[key])]
        }),
      ) as Record<any, any>

      let fmt: ModelFormat<T> = {}
      if (Object.keys(format).length) {
        fmt = format
      } else if (options.format) {
        fmt = typeof options.format === 'function' ? options.format(res) : options.format
      }
      // 合并
      if (fmt.assign) Object.assign(res, fmt.assign)
      // 空值转 null
      if (fmt.nullable === undefined || fmt.nullable) {
        Object.entries(res).forEach(([k, v]) => {
          if (v === undefined || (typeof v === 'string' && v.length === 0)) {
            // @ts-ignore
            res[k] = null
          }
        })
      }

      // 分页字段
      if (fmt.pageable) {
        if (fmt.ruoyi) {
          // @ts-ignore
          res.pageNum = fmt.pageable.value.page
          // @ts-ignore
          res.pageSize = fmt.pageable.value.size
        } else if (isPhpService) {
          res.pageinfo = {
            page: fmt.pageable.value.page,
            perpage: fmt.pageable.value.size,
          }
        } else {
          res.pageinfo = {
            pageNum: fmt.pageable.value.page,
            pageSize: fmt.pageable.value.size,
          }
        }
      }
      // 字段排序
      if (fmt.ruoyi && fmt.sortable?.value) {
        res.orderByColumn = fmt.sortable.value.key
        res.isAsc = fmt.sortable.value.order === 'asc' ? 'ascending' : 'descending'
      }

      // 范围类型转换
      if (fmt.range) {
        if (Array.isArray(fmt.range)) {
          fmt.range.forEach((it) => {
            const key = (Array.isArray(it) ? it[0] : it) as string
            const range: string[] = Array.isArray(res[key]) ? res[key] : []
            if (Array.isArray(it)) {
              if (it.length === 2) {
                rangeParamsFormat(!!fmt.ruoyi, res, range, [key, `begin${it[1]}`, `end${it[1]}`])
              } else {
                rangeParamsFormat(!!fmt.ruoyi, res, range, [key, it[1], it[2]])
              }
            } else {
              rangeParamsFormat(!!fmt.ruoyi, res, range, [key, `begin${upperFirst(key)}`, `end${upperFirst(key)}`])
            }
          })
        } else {
          const range = Array.isArray(res[fmt.range]) ? res[fmt.range] : []
          rangeParamsFormat(!!fmt.ruoyi, res, range, [fmt.range, 'beginTime', 'endTime'])
        }
      }
      // 获取指定 key
      if (fmt.pick) return pick(res, fmt.assign ? [Object.keys(fmt.assign), fmt.pick].flat() : fmt.pick) as R
      // 排除指定 key
      if (fmt.omit) return omit(res, fmt.omit) as R
      return res
    },
  })

  Object.defineProperty(model, 'validateModel', {
    value<R extends(T & Record<string, any>)>(rules?: ModelRules<T> | Ref<ModelRules<T>>, format?: ModelFormat<T>): Promise<R> {
      const model = Object.fromEntries(
        Object.keys(defaultModel).map((key) => {
          return [key, this[key]]
        }),
      ) as R

      if (format?.assign) Object.assign(model, format.assign)

      let pres
      const ref = this.getRef()
      if (ref) {
        pres = ref.validate()
      } else if (rules) {
        const schema = new Schema(unref(rules) as any)
        pres = schema.validate(model)
      } else if (options.rules) {
        const schema = new Schema(options.rules as any)
        pres = schema.validate(model)
      } else {
        return Promise.resolve(model)
      }
      return pres
        .then(() => {
          return this.getModel(format)
        })
        .catch((reason: ValidateFieldsError) => {
          const msg = Object.values(reason)[0][0].message || ''
          ElMessage.error(msg)
          return Promise.reject(reason)
        })
    },
  })

  Object.defineProperty(model, 'getReqAble', {
    value(): Partial<PageInfo> {
      const fmt = typeof options.format === 'function' ? options.format(this) : options.format

      return {
        ...fmt?.pageable?.value.getReq(),
        ...fmt?.sortable?.value.getReq(),
      }
    },
  })

  return reactive(model)
}
